package MawsonClient;

import MawsonClient.GUI.AboutDialog;
import MawsonClient.GUI.MawsonFrame;
import MawsonClient.GUI.RequestedDataElement;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.*;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.net.*;
import java.security.MessageDigest;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.Security;
import java.util.*;
import java.util.Timer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * User: Ryan Green
 * Date: Nov 5, 2010
 * Time: 12:44:49 PM
 * Copyright - University of South Australia
 */
public class Mawson {
	String configFile = "config/fileParser.xml";
	String pathsFile = "config/pathConfig.xml"; //the config file

	String hashFileName = "config/hashFile.dat";
	String trayIconFile = "config/mawson24.gif";

	String logFile = "config/log.txt";
	LogHandler log;

	String currentHash;

	MessageDigest digest;
	HashSet<String> hashList;

	public ArrayList<HashMap<Integer, String>> inputFile;

	Config config;

	public static String newline = System.getProperty("line.separator");

	Mawson(String[] args) {

		//for debugging uses - set config and paths files at runtime
		if (args.length == 1) {
			configFile = args[0];
		} else if (args.length == 2) {
			configFile = args[0];
			pathsFile = args[1];
		}

		//deal with the log file
		log = new LogHandler(logFile);

		config = new Config(configFile, pathsFile, log);

		final TrayIcon trayIcon;

		if (SystemTray.isSupported()) {

			SystemTray tray = SystemTray.getSystemTray();
			Image image = Toolkit.getDefaultToolkit().getImage(trayIconFile);

			ActionListener exitListener = new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					log.LogAndExit("Exiting...");
				}
			};

			ActionListener aboutListener = new ActionListener() {

				public void actionPerformed(ActionEvent actionEvent) {
					new AboutDialog(log);
				}
			};

			ActionListener manualAddListener = new ActionListener() {

				public void actionPerformed(ActionEvent actionEvent) {
					JFileChooser chooser = new JFileChooser();
					if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION)
						handleFile(chooser.getSelectedFile());
				}
			};

			PopupMenu popup = new PopupMenu();

			MenuItem addItem = new MenuItem("Manually Add Item");
			MenuItem aboutItem = new MenuItem("About");
			MenuItem defaultItem = new MenuItem("Exit");
			defaultItem.addActionListener(exitListener);
			aboutItem.addActionListener(aboutListener);
			addItem.addActionListener(manualAddListener);
			popup.add(addItem);
			popup.add(aboutItem);
			popup.add(defaultItem);

			trayIcon = new TrayIcon(image, "MawsonClient.Mawson Client", popup);

			ActionListener actionListener = new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					trayIcon.displayMessage("Action Event",
									"An Action Event Has Been Performed!",
									TrayIcon.MessageType.INFO);
				}
			};

			trayIcon.setImageAutoSize(true);
			trayIcon.addActionListener(actionListener);

			try {
				tray.add(trayIcon);

				//setup the message digest
				digest = MessageDigest.getInstance("MD5");

				//setup the hash list
				File hashFile = new File(hashFileName);
				if (!hashFile.exists()) hashFile.createNewFile();

				BufferedReader in = new BufferedReader(new FileReader(hashFile.getAbsolutePath())); //reads the input file
				hashList = new HashSet<String>(); //hold the 2D array of values

				String line;
				while ((line = in.readLine()) != null) {
					hashList.add(line);
				}
				in.close();

			} catch (Exception e) {
				log.LogAndExit("System tray not supported", true);
			}
		} else {
			log.LogAndExit("System tray not supported", true);
		}

		watch(config.watchFolder, config.watchFilter);
	}

	public void handleFile(File file) {
		try {
			InputStream is = new FileInputStream(file);

			//check hash
			byte[] bytes = new byte[8192];

			digest.reset();

			// Read in the bytes
			int read;
			while ((read = is.read(bytes)) > 0) {
				digest.update(bytes, 0, read);
			}

			is.close();

			String md5 = convertToHex(digest.digest());

			if (hashList.contains(md5)) {
				if (JOptionPane.showConfirmDialog(null, "It seems this file has already been added to the database - do you wish to continue?", "Confirm", JOptionPane.YES_NO_OPTION) == JOptionPane.NO_OPTION)
					return;
			} else {
				currentHash = md5;
			}

			digest.reset();

			//load the deliminated file
			BufferedReader in = new BufferedReader(new FileReader(file.getAbsolutePath())); //reads the input file
			inputFile = new ArrayList<HashMap<Integer, String>>(); //hold the 2D array of values

			String line;
			if (config.ignoreFirstLine) in.readLine();

			while ((line = in.readLine()) != null) {
				String[] tokens = line.split(config.inputDeliminator); //split the line into tokens
				inputFile.add(new HashMap<Integer, String>()); //create a new array to hold the tokens
				for (int i = 0; i < config.numColumns; i++) {
					if (i < tokens.length)
						inputFile.get(inputFile.size() - 1).put(i, tokens[i]); //add each token to the array
					else
						inputFile.get(inputFile.size() - 1).put(i, ""); //add in empty strings for empty fields
				}
			}

			in.close();

			//remove unmatched elements
			for (Iterator<HashMap<Integer, String>> i = inputFile.iterator(); i.hasNext(); ) {  //for each line in the file
				if (i.next().get(2).equalsIgnoreCase("Unmatched")) {
					i.remove();
				}
			}

			//add derived elements
			for (int j = 0; j < inputFile.size(); j++) {  //for each line in the file
				for (DerivedField dField : config.derivedFields) {
					String tmpString = inputFile.get(j).get(dField.baseIndex);
					for (String regex : dField.getRegex()) {
						tmpString = tmpString.replaceAll(regex, "");
					}
					if (!dField.checkRegex.equals("")) { //check the output against the checking regular expression
						if (!tmpString.matches(dField.checkRegex)) {
							log.Log("A dervied attribute failed a regular expression validity check.", true);
							return;
						}
					}
					inputFile.get(j).put(dField.index, tmpString);
				}
			}

			//sanitize by deleting
			for (Field field : config.fields) {
				if (field.delete) {
					for (int i = 0; i < inputFile.size(); i++) {
						inputFile.get(i).remove(field.index);
					}
				}
			}

			//if there is input requested from the user
			if (config.perFileElements.size() != 0 || config.perLineElements.size() != 0 || config.perAggElements.size() != 0) {
				ArrayList<ArrayList<RequestedDataElement>> perLineElements = new ArrayList<ArrayList<RequestedDataElement>>();
				ArrayList<ArrayList<RequestedDataElement>> perAggElements = new ArrayList<ArrayList<RequestedDataElement>>();
				HashMap<Integer, String> aggMap = new HashMap<Integer, String>();

				//do the per line requests - it will end up empty if there are no per line requested data elements
				if (config.perLineElements.size() > 0) {
					for (int i = 0; i < inputFile.size(); i++) {
						ArrayList<RequestedDataElement> templist = new ArrayList<RequestedDataElement>();
						perLineElements.add(templist);
						for (RequestedDataElement element : config.perLineElements) {
							templist.add(element.getOrigComponent());
						}
					}
				}

				if (config.perAggElements.size() != 0) {

					//so now we assume all aggregate requested elements are aggregated on the same index.
					Map<String, Integer> countMap = new HashMap<String, Integer>();
					for (int j = 0; j < inputFile.size(); j++) {  //for each line in the file
						String val = inputFile.get(j).get(config.perAggElements.get(0).aggIndex);
						aggMap.put(j, val);
						if (countMap.containsKey(val))
							countMap.put(val, countMap.get(val) + 1);
						else
							countMap.put(val, 1);
					}

					for (int i = 0; i < countMap.size(); i++) {
						ArrayList<RequestedDataElement> templist = new ArrayList<RequestedDataElement>();
						perAggElements.add(templist);
						for (RequestedDataElement element : config.perAggElements) {
							templist.add(element.getOrigComponent());
						}
					}
				}

				new MawsonFrame(this, file.getName(), config.perFileElements, perLineElements, perAggElements, aggMap, config, log);
			} else {
				//if no input is requested from the user just output and quit
				outputData();
			}
		} catch (Exception e) {
			log.Log("Error handling new file.", true);
			log.LogAndExit(e.getStackTrace());
		}
	}

	private static String convertToHex(byte[] data) {
		StringBuffer buf = new StringBuffer();
		for (int i = 0; i < data.length; i++) {
			int halfbyte = (data[i] >>> 4) & 0x0F;
			int two_halfs = 0;
			do {
				if ((0 <= halfbyte) && (halfbyte <= 9))
					buf.append((char) ('0' + halfbyte));
				else
					buf.append((char) ('a' + (halfbyte - 10)));
				halfbyte = data[i] & 0x0F;
			} while (two_halfs++ < 1);
		}
		return buf.toString();
	}

	public static void main(String[] args) {
		if (new JustOneLock().isAppActive()) {
			JOptionPane.showMessageDialog(null,
							"Only one instance of MawsonClient.Mawson is able to be run.\n If you believe this to be in error please\n delete the 'lock.tmp' file in the config directory.",
							"Error",
							JOptionPane.ERROR_MESSAGE);
			System.exit(0);
		}

		new Mawson(args);
	}

	public void watch(String folder, String filter) {
		TimerTask task = new DirWatcher(folder, filter) {
			protected void onChange(File file, String action) {
				//do the action here
				if (action.equals("add")) // a new file has been added
					handleFile(file);
			}
		};

		Timer timer = new Timer();
		timer.schedule(task, new Date(), 1000);

	}

	public void outputData() {
		try {

			//write output to temp zipped file
			log.Log(config.tempFile);
			ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(config.tempFile + ".zip")));
			out.putNextEntry(new ZipEntry("data.csv"));
			for (HashMap<Integer, String> hashMap : inputFile) {
				for (int i = 0; i <= config.numCompleteColumns; i++) {
					if (hashMap.containsKey(i)) {
						out.write(hashMap.get(i).getBytes(), 0, hashMap.get(i).getBytes().length);
						if (i != config.numCompleteColumns)
							out.write(config.outputDeliminator.getBytes(), 0, config.outputDeliminator.getBytes().length);
					}
				}
				out.write(newline.getBytes(), 0, newline.getBytes().length);
			}
			out.flush();
			out.close();

			//encrypt and output to
			Security.addProvider(new BouncyCastleProvider());
			FileInputStream keyIn = new FileInputStream(config.keyFile);
			FileOutputStream pgpout = new FileOutputStream(config.tempFile + ".zip.gpg");
			encryptFile(pgpout, config.tempFile + ".zip", readPublicKey(keyIn), false, true);

			//delete the zip file
			File fi = new File(config.tempFile + ".zip");
			fi.delete();

			uploadData();
		} catch (Exception e) {
			log.Log("An error occurred while compressing and encrypting the file.", true);
			log.Log(e);
		}
	}

	private void uploadData() {
		try {
			URL url = new URL(config.serverAddress);

			HttpURLConnection conn;
			if (!config.proxyAddress.equals("")){
				conn = (HttpURLConnection)url.openConnection(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(config.proxyAddress, Integer.parseInt(config.proxyPort))));
				
				//todo handle pw
				if (config.proxyUsername== null || config.proxyPassword == null) {
					if (!askProxyPass()) {
						config.proxyUsername = "";
						config.proxyPassword = "";
					}
				}
				conn.setRequestProperty("Proxy-Authorization", "Basic " + new sun.misc.BASE64Encoder().encode((config.proxyUsername + ":" + config.proxyPassword).getBytes()));
			} else {
				conn = (HttpURLConnection)url.openConnection();
			}

			//URLConnection conn = url.openConnection();
			conn.setRequestProperty("Authorization", "Basic " + new sun.misc.BASE64Encoder().encode((config.username + ":" + config.password).getBytes()));
            
	    conn.setDoOutput(true);
//			conn.setRequestMethod("GET");

			ClientHttpRequest req = new ClientHttpRequest(conn);
			req.setParameter("bUploading", "2");
			req.setParameter("DataSource", config.dataSource);
			req.setParameter("FileStream", new File(config.tempFile + ".zip.gpg"));
			req.setParameter("ConfigID", config.configID);
			InputStream response = req.post();
			BufferedInputStream bin = new BufferedInputStream(response);
			int b;
			String returnString = "";
			while ((b = bin.read()) != -1)
				returnString += (char) b;
			response.close();

//			BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
//
//			String line;
//			while ((line = br.readLine()) != null){
//				System.out.println(line);
//			}

			if (returnString.indexOf("ERROR") != -1) {

				if (returnString.indexOf("ERROR #822: Access Denied") != -1) {
					do {
						if (askUserPass()) {
							uploadData();
							return;
						} else {
							if (JOptionPane.showConfirmDialog(null, "Are you sure you want to end this session?", "Confirm", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION)
								return;
						}
					} while (true);
				} else {
					log.Log("An error was returned when uploading to the server.", true);
					log.Log(returnString);
				}
			} else {
				JOptionPane.showMessageDialog(null, "File successfully uploaded to the server.", "Notification", JOptionPane.INFORMATION_MESSAGE);
				log.Log(returnString);
                log.Log("File successfully uploaded to the server.");
			}

			//add the hash to the hashlist
			hashList.add(currentHash);
			writeHashFile();

			//todo check return string for confirmation msg and display message dialog if all is good
			//todo otherwise display message dialog of any error
			//todo log it either way
		} catch (Exception e) {
			//todo change this to reattempt server communication
			log.Log(e);
			if (e.toString().contains("407")){
				//zero out proxy username password in case they are incorrect;
				config.proxyPassword = null;
				config.proxyUsername = null;
			}
			log.Log("An error occurred when communicating with the server.", true);
			log.LogAndExit(e.getStackTrace());
		}
	}


	private static PGPPublicKey readPublicKey(
					InputStream in)
					throws IOException, PGPException {
		in = PGPUtil.getDecoderStream(in);

		PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(in);

		Iterator rIt = pgpPub.getKeyRings();

		while (rIt.hasNext()) {
			PGPPublicKeyRing kRing = (PGPPublicKeyRing) rIt.next();
			Iterator kIt = kRing.getPublicKeys();

			while (kIt.hasNext()) {
				PGPPublicKey k = (PGPPublicKey) kIt.next();

				if (k.isEncryptionKey()) {
					return k;
				}
			}
		}

		throw new IllegalArgumentException("Can't find encryption key in key ring.");
	}

	private static void encryptFile(
					OutputStream out,
					String fileName,
					PGPPublicKey encKey,
					boolean armor,
					boolean withIntegrityCheck)
					throws IOException, NoSuchProviderException {
		if (armor) {
			out = new ArmoredOutputStream(out);
		}

		try {
			ByteArrayOutputStream bOut = new ByteArrayOutputStream();


			PGPCompressedDataGenerator comData = new PGPCompressedDataGenerator(
							PGPCompressedData.ZIP);

			PGPUtil.writeFileToLiteralData(comData.open(bOut), PGPLiteralData.BINARY, new File(fileName));

			comData.close();

			PGPEncryptedDataGenerator cPk = new PGPEncryptedDataGenerator(PGPEncryptedData.CAST5, withIntegrityCheck, new SecureRandom(), "BC");

			cPk.addMethod(encKey);

			byte[] bytes = bOut.toByteArray();

			OutputStream cOut = cPk.open(out, bytes.length);

			cOut.write(bytes);

			cOut.close();

			out.close();
		} catch (PGPException e) {
			System.err.println(e);
			if (e.getUnderlyingException() != null) {
				e.getUnderlyingException().printStackTrace();
			}
		}
	}

	private boolean askUserPass() {
		JPanel panel = new JPanel();

		panel.setLayout(new GridLayout(4, 1));

		JLabel username = new JLabel("Username");
		JLabel password = new JLabel("Password");

		JTextField textField = new JTextField(20);
		JPasswordField passwordField = new JPasswordField(20);

		panel.add(username);
		panel.add(textField);
		panel.add(password);
		panel.add(passwordField);

		if (JOptionPane.showConfirmDialog(null, panel, "Incorrect Username or Password", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE) == JOptionPane.OK_OPTION) {
			config.username = textField.getText();
			config.password = new String(passwordField.getPassword());
			return true;
		}

		return false;
	}

	private void writeHashFile() {
		try {
			// Create file
			BufferedWriter out = new BufferedWriter(new FileWriter(hashFileName));
			Iterator itr = hashList.iterator();
			while (itr.hasNext())
				out.write(itr.next() + "\n");

			//Close the output stream
			out.close();
		} catch (Exception e) {//Catch exception if any
			log.Log("An error occurred when writing to the hash log.", true);
			log.Log(e.getStackTrace());
		}
	}

	private boolean askProxyPass() {
		JPanel panel = new JPanel();

		panel.setLayout(new GridLayout(4, 1));

		JLabel username = new JLabel("Username");
		JLabel password = new JLabel("Password");

		JTextField textField = new JTextField(20);
		JPasswordField passwordField = new JPasswordField(20);

		panel.add(username);
		panel.add(textField);
		panel.add(password);
		panel.add(passwordField);

		if (JOptionPane.showConfirmDialog(null, panel, "Please enter your proxy username & password", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE) == JOptionPane.OK_OPTION) {
			config.proxyUsername = textField.getText();
			config.proxyPassword = new String(passwordField.getPassword());
			return true;
		}

		return false;
	}
}
